package pl.edu.mimuw.irs.core.model.analysis.dd;

import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import pl.edu.mimuw.irs.core.exception.InternalException;
import pl.edu.mimuw.irs.core.exception.InternalException.Cause;
import pl.edu.mimuw.irs.core.model.analysis.IListener;
import pl.edu.mimuw.irs.core.model.analysis.IRegistry;
import pl.edu.mimuw.irs.core.model.analysis.RequirementPlace;
import pl.edu.mimuw.irs.core.model.analysis.ResourcePlace;
import pl.edu.mimuw.irs.core.model.analysis.Time;
import pl.edu.mimuw.irs.core.model.design.IRSActivity;
import pl.edu.mimuw.irs.core.model.design.IRSNodeInterface;
import pl.edu.mimuw.irs.core.model.design.processing.Requirement;
import pl.edu.mimuw.irs.core.util.Converter;

public class ActivityWrapper implements IListener {
	
	// *** Attributes
	
	private IRSActivity activity;
	/* 
	 * This is the leaf of the parent tree. We can find the direct above activity
	 * by traversing it upwards.
	 */
	private Map<Requirement, Dependency> dependencies;
	/* 
	 * This is the leaf of the parent tree. We can find the direct above activity
	 * by traversing it upwards.
	 */
	private PathNode ascendingPath;
	/*
	 * What resources are currently involved in this activity.
	 */
	private Set<ResourcePlace> involvedResources;
	/* 
	 * This is the root of the children tree. We can find the direct below activities
	 * by traversing it downwards.
	 */
	private PathNode descendingPath;
	private boolean ancestryCreated;
	private boolean started;
	private boolean registered;
	private Time rootTimestamp;
	private Time timestamp;
	/*
	 * Cached value of isDeadlocking() result.
	 */
	private Boolean cachedValue;
	
	public IRSActivity getActivity() {
		return activity;
	}
	public Map<Requirement, Dependency> getDependencies() {
		if (dependencies == null) dependencies = 
			new LinkedHashMap<Requirement, Dependency>();
		return dependencies;
	}
	public Set<ResourcePlace> getInvolvedResources() {
		if (involvedResources == null) involvedResources = new HashSet<ResourcePlace>();
		return involvedResources;
	}
	public Set<ResourcePlace> getDerivedResources() {
		Set<ResourcePlace> derived = new HashSet<ResourcePlace>();
		derived.addAll(getInvolvedResources());
		if (getAscendingPath() != null) derived.addAll(getAscendingPath().getDerivedResources());
		return derived;
	}
	public void setAscendingPath(PathNode ascendingPath) {
		this.ascendingPath = ascendingPath;
	}
	public PathNode getAscendingPath() {
		return ascendingPath;
	}
	public PathNode getDescendingPath() {
		return descendingPath;
	}
	public boolean isStarted() {
		return started;
	}
	public void setStarted(boolean started) {
		this.started = started;
		/* 
		 * When "started" state is changed, we want to inform on which path it 
		 * happened.
		 */
		if (getAscendingPath() != null) getAscendingPath().setStarted(started);
		/*
		 * Listen for changes on descending requirements.
		 */
		if (!isRegistered()) {
			for (Entry<Requirement, Dependency> entry : getDependencies().entrySet()) {
				entry.getValue().getRequirementPlace().register(
						RequirementPlace.DECREASED_TOKENS_NUMBER, this);
			}
			registered = true;
		}
		/*
		 * Mark when the job begins/ends.
		 */
		if (started) {
			if (getAscendingPath() != null) {
				this.timestamp = getAscendingPath().getRoot().getOwner().getTimestamp().copy();
			} else {
				this.timestamp = getRootTimestamp().copy();
			}
		} else {
			Time timestamp;
			if (getAscendingPath() != null) {
				timestamp = getAscendingPath().getRoot().getOwner().getTimestamp();
			} else {
				timestamp = getRootTimestamp();
			}
			this.timestamp.add(getActivity().getTimeUnits());
			timestamp.setMax(getTimestamp());
		}
	}
	public boolean isRegistered() {
		return registered;
	}
	public Time getRootTimestamp() {
		return rootTimestamp;
	}
	public void setRootTimestamp(Time rootTimestamp) {
		this.rootTimestamp = rootTimestamp;
	}
	public Time getTimestamp() {
		if (timestamp == null) timestamp = new Time();
		return timestamp;
	}
	public Boolean getCachedValue() {
		return cachedValue;
	}
	public void clearCache() {
		cachedValue = null;
		PathNode pathNode = getAscendingPath();
		if (pathNode != null) pathNode.clearCache();
	}

	// *** Constructors
	
	public ActivityWrapper(IRSActivity activity) {
		this.activity = activity;
	}
	
	// *** Methods
	
	public void reset() {
		this.timestamp = new Time();
	}
	
	public void createDescendingPath(Map<IRSActivity, ActivityWrapper> wrappers) {
		/* Create ancestry tree only for the first time. */
		if (!ancestryCreated) {
			IRSActivity activity = getActivity();
			PathNode path = new PathNode(activity.getFragmentation());
			createDescendingPath(activity, path, wrappers);
			this.descendingPath = simplifyDescendingPath(path);
			this.descendingPath.setOwner(this);
			ancestryCreated = true;
		}
		/* Else the ancestry tree creation has already been performed. */
	}
	
	private void createDescendingPath(IRSNodeInterface parentNode, PathNode parentPath,
			Map<IRSActivity, ActivityWrapper> wrappers) {

		switch (parentNode.getFragmentation().getType()) {
		case ALTERNATIVE:
		case SEQUENCE:
		case CONCURRENCY:
			for (IRSNodeInterface node : parentNode.getChildren()) {
				PathNode path = new PathNode(parentPath, node.getFragmentation());
				createDescendingPath(node, path, wrappers);
			}
			break;
		case ACTIVITY:
			/* 
			 * If we find an activity, then stop building path - pass it 
			 * to new activity.
			 */
			for (IRSNodeInterface node : parentNode.getChildren()) {
				if (node instanceof IRSActivity) {
					IRSActivity activity = Converter.asIRSActivity(node);
					ActivityWrapper wrapper = wrappers.get(activity);
					PathNode activityNode = 
						new PathNode(parentPath, activity.getFragmentation(), 
								wrapper);
					wrapper.createDescendingPath(wrappers);
					wrapper.setAscendingPath(activityNode);
					wrapper.setRootTimestamp(getRootTimestamp());
				}
			}
			break;
		default:
			/*
			 * If fragmentation is NONE we stop building path.
			 */
			// TODO : Iteration...
			break;
		}
		
	}
	
	private PathNode simplifyDescendingPath(PathNode descendingPath) {
		/*
		 * Remove dead ends (paths which do not end on Activity). For
		 * later usage the NullActivityWrapper nodes will be made for
		 * dead ending subtree - to distinguish situations like 
		 * ALTERNATIVE[Path(Activity)] and ALTERNATIVE[Path(Activity), 
		 * DeadEnd].
		 * This is because ALTERNATIVE[Path(Activity), DeadEnd] does not 
		 * require anything to be finished, where ALTERNATIVE[Path(Activity)]
		 * does.
		 */
		List<PathNode> newChildren;
		switch (descendingPath.getFragmentation().getType()) {
		case ALTERNATIVE:
			/*
			 * In alternative we want to know if there exist at least one
			 * dead end. If yes - we want to mark it by putting a dead end
			 * between the children, so we would know, that this node can end
			 * without any further requirements.
			 */
			PathNode deadEnd = null;
			newChildren = new LinkedList<PathNode>();
			for (PathNode child : descendingPath.getChildren()) {
				PathNode simplePath = simplifyDescendingPath(child);
				if (deadEnd == null && simplePath.isDeadEnd()) {
					deadEnd = simplePath;
				} else if (!simplePath.isDeadEnd()) {
					newChildren.add(simplePath);
				}
			}
			if (newChildren.size() == 0) {
				if (deadEnd == null) {
					/*
					 * Impossible situation.
					 */
					throw new InternalException(Cause.WORKFLOW_FRAGMENTATION_NO_CHILDREN, 
							descendingPath.getFragmentation().toString());
				} else {
					/*
					 * Only dead ends in this node, so whole node is a dead end.
					 */
					return new DeadEnd();
				}
			} else {
				if (deadEnd == null) {
					/* 
					 * Create node with new children.
					 */
					return new PathNode(descendingPath.getFragmentation(),
							newChildren);
				} else {
					/* 
					 * Add dead end to the children, so we can distinguish
					 * situations mentioned in the beginning of this method.
					 */
					newChildren.add(deadEnd);
					return new PathNode(descendingPath.getFragmentation(),
							newChildren);
				}
			}
		case SEQUENCE:
		case CONCURRENCY:
			/*
			 * Here we don't care about dead ends - if exists, we just 
			 * omit them.
			 */
			newChildren = new LinkedList<PathNode>();
			for (PathNode child : descendingPath.getChildren()) {
				PathNode simplePath = simplifyDescendingPath(child);
				if (!simplePath.isDeadEnd()) {
					newChildren.add(simplePath);
				}
			}
			if (newChildren.size() == 0) {
				/*
				 * All children are dead ends. So this node is a dead end
				 * as well.
				 */
				return new DeadEnd();
			} else {
				/* 
				 * Create node with new children.
				 */
				return new PathNode(descendingPath.getFragmentation(),
						newChildren);
			}
		case ACTIVITY:
			/* 
			 * We do not need to store information about activity
			 * fragmentation. This is already known in path because its
			 * activity is not null.
			 */
			for (PathNode child : descendingPath.getChildren()) {
				if (child.getActivity() != null) {
					return child;
				}
			}
		default:
			// TODO : ITERATION...
			return new DeadEnd();
		}
	}

	@Override
	public String toString() {
		return "*" + activity.toString() + "*";
	}

	public boolean isAncestorOf(IRSActivity irsActivity) {
		return getActivity().isAncestorOf(irsActivity);
	}

	public boolean isAncestorOf(ActivityWrapper activity) {
		return isAncestorOf(activity.getActivity());
	}
	
	@Override
	public void propertyChanged(Object propertyId, Object newValue,
			IRegistry registry) {
		if (propertyId.equals(RequirementPlace.DECREASED_TOKENS_NUMBER)) {
			/*
			 * This means some of the requirements we need has changed. So if we had
			 * cached result, we have to clear it.
			 */
			clearCache();
		}
	}
}
